/*
 * Copyright (C) 2007 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#undef LOG_TAG
#define LOG_TAG "CursorWindow"

#include <jni.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>

#include "CursorWindow.h"
#include "jni_elements.h"
#include "jni_exception.h"
#include "sqlite3_exception.h"

#include <wchar.h>
#include <stdlib.h>

#include <stdint.h>
#include <inttypes.h>

namespace sqlcipher {

  static jfieldID gWindowField;
  static jfieldID gBufferField;
  static jfieldID gSizeCopiedField;

#define GET_WINDOW(env, object) ((CursorWindow *)env->GetLongField(object, gWindowField))
#define SET_WINDOW(env, object, window) (env->SetLongField(object, gWindowField,(intptr_t)window))
#define SET_BUFFER(env, object, buf) (env->SetObjectField(object, gBufferField, buf))
#define SET_SIZE_COPIED(env, object, size) (env->SetIntField(object, gSizeCopiedField, size))

  CursorWindow * get_window_from_object(JNIEnv * env, jobject javaWindow)
  {
    return GET_WINDOW(env, javaWindow);
  }

  static void native_init_empty(JNIEnv * env, jobject object,
                                jboolean localOnly, jlong initialSize,
                                jlong growthPaddingSize, jlong maxSize)
  {
    uint8_t * data;
    size_t size;
    CursorWindow * window;

    window = new CursorWindow(initialSize, growthPaddingSize, maxSize);
    if (!window) {
      jniThrowException(env, "java/lang/RuntimeException", "No memory for native window object");
      return;
    }

    if (!window->initBuffer(localOnly)) {
      jniThrowException(env, "java/lang/IllegalStateException", "Couldn't init cursor window");
      delete window;
      return;
    }
    LOG_WINDOW("native_init_empty: window = %p", window);
    SET_WINDOW(env, object, window);
  }

  static void native_clear(JNIEnv * env, jobject object)
  {
    CursorWindow * window = GET_WINDOW(env, object);
    LOG_WINDOW("Clearing window %p", window);
    if (window == NULL) {
      jniThrowException(env, "java/lang/IllegalStateException", "clear() called after close()");
      return;
    }
    window->clear();
  }

  static void native_close(JNIEnv * env, jobject object)
  {
    CursorWindow * window = GET_WINDOW(env, object);
    if (window) {
      LOG_WINDOW("Closing window %p", window);
      delete window;
      SET_WINDOW(env, object, 0);
    }
  }

  static void throwExceptionWithRowCol(JNIEnv * env, jint row, jint column)
  {
    char buf[100];
    snprintf(buf, sizeof(buf), "get field slot from row %d col %d failed", row, column);
    jniThrowException(env, "net/sqlcipher/InvalidRowColumnException", buf);
  }

  static void throwUnknowTypeException(JNIEnv * env, jint type)
  {
    char buf[80];
    snprintf(buf, sizeof(buf), "UNKNOWN type %d", type);
    jniThrowException(env, "net/sqlcipher/UnknownTypeException", buf);
  }

  static jlong getLong_native(JNIEnv * env, jobject object, jint row, jint column)
  {
    int32_t err;
    CursorWindow * window = GET_WINDOW(env, object);
    LOG_WINDOW("Getting long for %d,%d from %p", row, column, window);

    field_slot_t field;
    err = window->read_field_slot(row, column, &field);
    if (err != 0) {
      throwExceptionWithRowCol(env, row, column);
      return 0;
    }

    uint8_t type = field.type;
    if (type == FIELD_TYPE_INTEGER) {
      int64_t value;
      if (window->getLong(row, column, &value)) {
        return value;
      }
      return 0;
    } else if (type == FIELD_TYPE_STRING) {
      uint32_t size = field.data.buffer.size;
      if (size > 0) {
        long long int result;
        jstring data = env->NewString((const jchar*)window->offsetToPtr(field.data.buffer.offset), (jsize)size / sizeof(jchar));
        const char* utf8data = env->GetStringUTFChars(data, NULL);
        result = strtoll(utf8data, NULL, 0);
        if(utf8data) env->ReleaseStringUTFChars(data, utf8data);
        if(data) env->DeleteLocalRef(data);
        return result;
      } else {
        return 0;
      }
    } else if (type == FIELD_TYPE_FLOAT) {
      double value;
      if (window->getDouble(row, column, &value)) {
        return value;
      }
      return 0;
    } else if (type == FIELD_TYPE_NULL) {
      return 0;
    } else if (type == FIELD_TYPE_BLOB) {
      throw_sqlite3_exception(env, "Unable to convert BLOB to long");
      return 0;
    } else {
      throwUnknowTypeException(env, type);
      return 0;
    }
  }

  static jbyteArray getBlob_native(JNIEnv* env, jobject object, jint row, jint column)
  {
    int32_t err;
    CursorWindow * window = GET_WINDOW(env, object);
    LOG_WINDOW("Getting blob for %d,%d from %p", row, column, window);

    field_slot_t field;
    err = window->read_field_slot(row, column, &field);
    if (err != 0) {
      throwExceptionWithRowCol(env, row, column);
      return NULL;
    }

    uint8_t type = field.type;
    if (type == FIELD_TYPE_BLOB || type == FIELD_TYPE_STRING) {
      jbyteArray byteArray = env->NewByteArray(field.data.buffer.size);
      if(byteArray == NULL) return NULL;
      env->SetByteArrayRegion(byteArray, 0, field.data.buffer.size,
                              (const jbyte*)window->offsetToPtr(field.data.buffer.offset));
      return byteArray;
    } else if (type == FIELD_TYPE_INTEGER) {
      throw_sqlite3_exception(env, "INTEGER data in getBlob_native ");
    } else if (type == FIELD_TYPE_FLOAT) {
      throw_sqlite3_exception(env, "FLOAT data in getBlob_native ");
    } else if (type == FIELD_TYPE_NULL) {
      // do nothing
    } else {
      throwUnknowTypeException(env, type);
    }
    return NULL;
  }

  static jboolean isBlob_native(JNIEnv* env, jobject object, jint row, jint column)
  {
    int32_t err;
    CursorWindow * window = GET_WINDOW(env, object);
    LOG_WINDOW("Checking if column is a blob or null for %d,%d from %p", row, column, window);
    field_slot_t field;
    err = window->read_field_slot(row, column, &field);
    if (err != 0) {
      throwExceptionWithRowCol(env, row, column);
      return false;
    }
    return field.type == FIELD_TYPE_BLOB || field.type == FIELD_TYPE_NULL;
  }

  static jboolean isString_native(JNIEnv* env, jobject object, jint row, jint column)
  {
    int32_t err;
    CursorWindow * window = GET_WINDOW(env, object);
    LOG_WINDOW("Checking if column is a string or null for %d,%d from %p", row, column, window);
    field_slot_t field;
    err = window->read_field_slot(row, column, &field);
    if (err != 0) {
      throwExceptionWithRowCol(env, row, column);
      return false;
    }
    return field.type == FIELD_TYPE_STRING || field.type == FIELD_TYPE_NULL;
  }

  static jboolean isInteger_native(JNIEnv* env, jobject object, jint row, jint column)
  {
    int32_t err;
    CursorWindow * window = GET_WINDOW(env, object);
    LOG_WINDOW("Checking if column is an integer for %d,%d from %p", row, column, window);
    field_slot_t field;
    err = window->read_field_slot(row, column, &field);
    if (err != 0) {
      throwExceptionWithRowCol(env, row, column);
      return false;
    }
    return field.type == FIELD_TYPE_INTEGER;
  }

  static jint getType_native(JNIEnv* env, jobject object, jint row, jint column)
  {
    int32_t err;
    CursorWindow * window = GET_WINDOW(env, object);
    LOG_WINDOW("Getting type for %d,%d from %p", row, column, window);
    field_slot_t field;
    err = window->read_field_slot(row, column, &field);
    if (err != 0) {
      throwExceptionWithRowCol(env, row, column);
      return false;
    }
    return field.type;
  }

  static jboolean isFloat_native(JNIEnv* env, jobject object, jint row, jint column)
  {
    int32_t err;
    CursorWindow * window = GET_WINDOW(env, object);
    LOG_WINDOW("Checking if column is a float for %d,%d from %p", row, column, window);
    field_slot_t field;
    err = window->read_field_slot(row, column, &field);
    if (err != 0) {
      throwExceptionWithRowCol(env, row, column);
      return false;
    }
    return field.type == FIELD_TYPE_FLOAT;
  }

  static jstring getString_native(JNIEnv* env, jobject object, jint row, jint column)
  {
    int i;
    int32_t err;
    CursorWindow * window = GET_WINDOW(env, object);
    LOG_WINDOW("Getting string for %d,%d from %p", row, column, window);
    field_slot_t field;
    err = window->read_field_slot(row, column, &field);
    if (err != 0) {
      throwExceptionWithRowCol(env, row, column);
      return NULL;
    }
    uint8_t type = field.type;
    jint size = (jint)field.data.buffer.size;
    if (type == FIELD_TYPE_NULL) {
      return NULL;
    } else if (type == FIELD_TYPE_BLOB) {
      throw_sqlite3_exception(env, "Unable to convert BLOB to string");
      return NULL;
    } else if (type == FIELD_TYPE_STRING) {
      return env->NewString((const jchar*)window->offsetToPtr(field.data.buffer.offset), (jsize)size / sizeof(jchar));
    } else if (type == FIELD_TYPE_INTEGER) {
      int64_t value;
      if (window->getLong(row, column, &value)) {
        char buf[32];
        snprintf(buf, sizeof(buf), "%" PRId64 "", value);
        return env->NewStringUTF((const char*)buf);
      }
      return NULL;
    } else if (type == FIELD_TYPE_FLOAT) {
      double value;
      if (window->getDouble(row, column, &value)) {
        char buf[32];
        snprintf(buf, sizeof(buf), "%g", value);
        return env->NewStringUTF(buf);
      }
    }
    return NULL;
  }

  /**
   * Use this only to convert characters that are known to be within the
   * 0-127 range for direct conversion to UTF-16
   */
  static jint charToJchar(const char* src, jchar* dst, jint bufferSize)
  {
    int32_t len = strlen(src);
    if (bufferSize < len) {
      len = bufferSize;
    }
    for (int i = 0; i < len; i++) {
      *dst++ = (*src++ & 0x7F);
    }
    return len;
  }

  static jcharArray copyStringToBuffer_native(JNIEnv* env, jobject object, jint row,
                                              jint column, jint bufferSize, jobject buf)
  {
    int32_t err;
    CursorWindow * window = GET_WINDOW(env, object);
    LOG_WINDOW("Copying string for %d,%d from %p", row, column, window);

    field_slot_t field;
    err = window->read_field_slot(row, column, &field);
    if (err != 0) {
      jniThrowException(env, "java/lang/IllegalStateException", "Unable to get field slot");
      return NULL;
    }

    jcharArray buffer = (jcharArray)env->GetObjectField(buf, gBufferField);
    if (buffer == NULL) {
      jniThrowException(env, "java/lang/IllegalStateException", "buf should not be null");
      return NULL;
    }
    jchar* dst = env->GetCharArrayElements(buffer, NULL);
    uint8_t type = field.type;
    uint32_t sizeCopied = 0;
    jcharArray newArray = NULL;
    if (type == FIELD_TYPE_STRING) {
      uint32_t size = field.data.buffer.size;
      if (size > 0) {
        jsize length = (jsize)size/sizeof(jchar);
        int32_t strSize = (jsize)size/sizeof(jchar);
        jstring content = env->NewString((const jchar *)window->offsetToPtr(field.data.buffer.offset), length);
        const jchar *elements = env->GetStringChars(content, JNI_FALSE);
        if (strSize > bufferSize || dst == NULL) {
          newArray = env->NewCharArray(length);
          env->SetCharArrayRegion(newArray, 0, length, elements);
          if(elements) env->ReleaseStringChars(content, elements);
          if(content) env->DeleteLocalRef(content);
        } else {
          memcpy(dst, elements, strSize * 2);
        }
        sizeCopied = strSize;
      }
    } else if (type == FIELD_TYPE_INTEGER) {
      int64_t value;
      if (window->getLong(row, column, &value)) {
        int len;
        char buf[32];
        len = snprintf(buf, sizeof(buf), "%" PRId64 "", value);
        jint bufferLength = env->GetArrayLength(buffer);
        if(len > bufferLength || dst == NULL){
          jstring content = env->NewStringUTF(buf);
          const jchar *elements = env->GetStringChars(content, JNI_FALSE);
          newArray = env->NewCharArray(len);
          env->SetCharArrayRegion(newArray, 0, len, elements);
          sizeCopied = len;
          if(elements) env->ReleaseStringChars(content, elements);
          if(content) env->DeleteLocalRef(content);
        } else {
          memcpy(dst, buf, len);
          sizeCopied = charToJchar(buf, dst, bufferSize);
        }
      }
    } else if (type == FIELD_TYPE_FLOAT) {
      double value;
      if (window->getDouble(row, column, &value)) {
        int len;
        char buf[32];
        len = snprintf(buf, sizeof(buf), "%g", value);
        jint bufferLength = env->GetArrayLength(buffer);
        if(len > bufferLength || dst == NULL){
          jstring content = env->NewStringUTF(buf);
          const jchar *elements = env->GetStringChars(content, JNI_FALSE);
          newArray = env->NewCharArray(len);
          env->SetCharArrayRegion(newArray, 0, len, elements);
          sizeCopied = len;
          if(elements) env->ReleaseStringChars(content, elements);
          if(content) env->DeleteLocalRef(content);
        } else {
          memcpy(dst, buf, len);
          sizeCopied = charToJchar(buf, dst, bufferSize);
        }
      }
    } else if (type == FIELD_TYPE_NULL) {
    } else if (type == FIELD_TYPE_BLOB) {
      throw_sqlite3_exception(env, "Unable to convert BLOB to string");
    } else {
      LOGE("Unknown field type %d", type);
      throw_sqlite3_exception(env, "UNKNOWN type in copyStringToBuffer_native()");
    }
    SET_SIZE_COPIED(env, buf, sizeCopied);
    env->ReleaseCharArrayElements(buffer, dst, JNI_OK);
    return newArray;
  }

  static jdouble getDouble_native(JNIEnv* env, jobject object, jint row, jint column)
  {
    int32_t err;
    CursorWindow * window = GET_WINDOW(env, object);
    LOG_WINDOW("Getting double for %d,%d from %p", row, column, window);

    field_slot_t field;
    err = window->read_field_slot(row, column, &field);
    if (err != 0) {
      throwExceptionWithRowCol(env, row, column);
      return 0.0;
    }

    uint8_t type = field.type;
    if (type == FIELD_TYPE_FLOAT) {
      double value;
      if (window->getDouble(row, column, &value)) {
        return value;
      }
      return 0.0;
    } else if (type == FIELD_TYPE_STRING) {
      uint32_t size = field.data.buffer.size;
      if (size > 0) {
        double result;
        jstring data = env->NewString((const jchar*)window->offsetToPtr(field.data.buffer.offset), (jsize)size / sizeof(jchar));
        const char* utf8data = env->GetStringUTFChars(data, NULL);
        result = strtod(utf8data, NULL);
        if(utf8data) env->ReleaseStringUTFChars(data, utf8data);
        if(data) env->DeleteLocalRef(data);
        return result;
      } else {
        return 0.0;
      }
    } else if (type == FIELD_TYPE_INTEGER) {
      int64_t value;
      if (window->getLong(row, column, &value)) {
        return (double) value;
      }
      return 0.0;
    } else if (type == FIELD_TYPE_NULL) {
      return 0.0;
    } else if (type == FIELD_TYPE_BLOB) {
      throw_sqlite3_exception(env, "Unable to convert BLOB to double");
      return 0.0;
    } else {
      throwUnknowTypeException(env, type);
      return 0.0;
    }
  }

  static jboolean isNull_native(JNIEnv* env, jobject object, jint row, jint column)
  {
    CursorWindow * window = GET_WINDOW(env, object);
    LOG_WINDOW("Checking for NULL at %d,%d from %p", row, column, window);
    bool isNull;
    if (window->getNull(row, column, &isNull)) {
      return isNull;
    }
    //TODO throw execption?
    return true;
  }

  static jint getNumRows(JNIEnv * env, jobject object)
  {
    CursorWindow * window = GET_WINDOW(env, object);
    return window->getNumRows();
  }

  static jboolean setNumColumns(JNIEnv * env, jobject object, jint columnNum)
  {
    CursorWindow * window = GET_WINDOW(env, object);
    return window->setNumColumns(columnNum);
  }

  static jboolean allocRow(JNIEnv * env, jobject object)
  {
    CursorWindow * window = GET_WINDOW(env, object);
    return window->allocRow() != NULL;
  }

  static jboolean putBlob_native(JNIEnv * env, jobject object, jbyteArray value, jint row, jint col)
  {
    CursorWindow * window = GET_WINDOW(env, object);
    if (!value) {
      LOG_WINDOW("How did a null value send to here");
      return false;
    }
    field_slot_t * fieldSlot = window->getFieldSlotWithCheck(row, col);
    if (fieldSlot == NULL) {
      LOG_WINDOW(" getFieldSlotWithCheck error ");
      return false;
    }

    jint len = env->GetArrayLength(value);
    int offset = window->alloc(len);
    if (!offset) {
      LOG_WINDOW("Failed allocating %u bytes", len);
      return false;
    }
    jbyte * bytes = env->GetByteArrayElements(value, NULL);
    window->copyIn(offset, (uint8_t const *)bytes, len);

    // This must be updated after the call to alloc(), since that
    // may move the field around in the window
    fieldSlot->type = FIELD_TYPE_BLOB;
    fieldSlot->data.buffer.offset = offset;
    fieldSlot->data.buffer.size = len;
    env->ReleaseByteArrayElements(value, bytes, JNI_ABORT);
    LOG_WINDOW("%d,%d is BLOB with %u bytes @ %d", row, col, len, offset);
    return true;
  }

  static jboolean putString_native(JNIEnv * env, jobject object, jstring value, jint row, jint col)
  {
    CursorWindow * window = GET_WINDOW(env, object);
    if (!value) {
      LOG_WINDOW("How did a null value send to here");
      return false;
    }
    field_slot_t * fieldSlot = window->getFieldSlotWithCheck(row, col);
    if (fieldSlot == NULL) {
      LOG_WINDOW(" getFieldSlotWithCheck error ");
      return false;
    }

#if WINDOW_STORAGE_UTF8
    int len = env->GetStringUTFLength(value) + 1;
    char const * valStr = env->GetStringUTFChars(value, NULL);
#else
    int len = env->GetStringLength(value);
    // GetStringLength return number of chars and one char takes 2 bytes
    len *= 2;
    const jchar* valStr = env->GetStringChars(value, NULL);
#endif
    if (!valStr) {
      LOG_WINDOW("value can't be transfer to UTFChars");
      return false;
    }

    int offset = window->alloc(len);
    if (!offset) {
      LOG_WINDOW("Failed allocating %u bytes", len);
#if WINDOW_STORAGE_UTF8
      env->ReleaseStringUTFChars(value, valStr);
#else
      env->ReleaseStringChars(value, valStr);
#endif
      return false;
    }

    window->copyIn(offset, (uint8_t const *)valStr, len);

    // This must be updated after the call to alloc(), since that
    // may move the field around in the window
    fieldSlot->type = FIELD_TYPE_STRING;
    fieldSlot->data.buffer.offset = offset;
    fieldSlot->data.buffer.size = len;

    LOG_WINDOW("%d,%d is TEXT with %u bytes @ %d", row, col, len, offset);
#if WINDOW_STORAGE_UTF8
    env->ReleaseStringUTFChars(value, valStr);
#else
    env->ReleaseStringChars(value, valStr);
#endif

    return true;
  }

  static jboolean putLong_native(JNIEnv * env, jobject object, jlong value, jint row, jint col)
  {
    CursorWindow * window = GET_WINDOW(env, object);
    if (!window->putLong(row, col, value)) {
      LOG_WINDOW(" getFieldSlotWithCheck error ");
      return false;
    }
    LOG_WINDOW("%d,%d is INTEGER 0x%016llx", row, col, value);
    return true;
  }

  static jboolean putDouble_native(JNIEnv * env, jobject object, jdouble value, jint row, jint col)
  {
    CursorWindow * window = GET_WINDOW(env, object);
    if (!window->putDouble(row, col, value)) {
      LOG_WINDOW(" getFieldSlotWithCheck error ");
      return false;
    }
    LOG_WINDOW("%d,%d is FLOAT %lf", row, col, value);
    return true;
  }

  static jboolean putNull_native(JNIEnv * env, jobject object, jint row, jint col)
  {
    CursorWindow * window = GET_WINDOW(env, object);
    if (!window->putNull(row, col)) {
      LOG_WINDOW(" getFieldSlotWithCheck error ");
      return false;
    }
    LOG_WINDOW("%d,%d is NULL", row, col);
    return true;
  }

  // free the last row
  static void freeLastRow(JNIEnv * env, jobject object) {
    CursorWindow * window = GET_WINDOW(env, object);
    window->freeLastRow();
  }

  static JNINativeMethod sMethods[] =
    {
      /* name, signature, funcPtr */
      {"native_init", "(ZJJJ)V", (void *)native_init_empty},
      // {"native_init", "(Landroid/os/IBinder;)V", (void *)native_init_memory},
      // {"native_getBinder", "()Landroid/os/IBinder;", (void *)native_getBinder},
      {"native_clear", "()V", (void *)native_clear},
      {"close_native", "()V", (void *)native_close},
      {"getLong_native", "(II)J", (void *)getLong_native},
      {"getBlob_native", "(II)[B", (void *)getBlob_native},
      {"isBlob_native", "(II)Z", (void *)isBlob_native},
      {"getString_native", "(II)Ljava/lang/String;", (void *)getString_native},
      //{"getString_native", "(II)[B", (void *)getString_native},
      {"copyStringToBuffer_native", "(IIILandroid/database/CharArrayBuffer;)[C", (void *)copyStringToBuffer_native},
      {"getDouble_native", "(II)D", (void *)getDouble_native},
      {"isNull_native", "(II)Z", (void *)isNull_native},
      {"getNumRows_native", "()I", (void *)getNumRows},
      {"setNumColumns_native", "(I)Z", (void *)setNumColumns},
      {"allocRow_native", "()Z", (void *)allocRow},
      {"putBlob_native", "([BII)Z", (void *)putBlob_native},
      {"putString_native", "(Ljava/lang/String;II)Z", (void *)putString_native},
      {"putLong_native", "(JII)Z", (void *)putLong_native},
      {"putDouble_native", "(DII)Z", (void *)putDouble_native},
      {"freeLastRow_native", "()V", (void *)freeLastRow},
      {"putNull_native", "(II)Z", (void *)putNull_native},
      {"isString_native", "(II)Z", (void *)isString_native},
      {"isFloat_native", "(II)Z", (void *)isFloat_native},
      {"isInteger_native", "(II)Z", (void *)isInteger_native},
      {"getType_native", "(II)I", (void *)getType_native},
    };

  int register_android_database_CursorWindow(JNIEnv * env)
  {
    jclass clazz;
    clazz = env->FindClass("net/sqlcipher/CursorWindow");
    if (clazz == NULL) {
      LOGE("Can't find net/sqlcipher/CursorWindow");
      return -1;
    }
    gWindowField = env->GetFieldID(clazz, "nWindow", "J");
    if (gWindowField == NULL) {
      LOGE("Error locating fields");
      return -1;
    }
    clazz =  env->FindClass("android/database/CharArrayBuffer");
    if (clazz == NULL) {
      LOGE("Can't find android/database/CharArrayBuffer");
      return -1;
    }
    gBufferField = env->GetFieldID(clazz, "data", "[C");
    if (gBufferField == NULL) {
      LOGE("Error locating fields data in CharArrayBuffer");
      return -1;
    }
    gSizeCopiedField = env->GetFieldID(clazz, "sizeCopied", "I");
    if (gSizeCopiedField == NULL) {
      LOGE("Error locating fields sizeCopied in CharArrayBuffer");
      return -1;
    }
    clazz = env->FindClass("net/sqlcipher/CursorWindow");
    return env->RegisterNatives(clazz, sMethods, NELEM(sMethods));
  }
} // namespace sqlcipher
